﻿using System;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Ink;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Shapes;

using Microsoft.Windows.Controls.Specialized.TextModel;
using Microsoft.Windows.Controls.Specialized.TextExtensions;
using System.Collections.Generic;

namespace Microsoft.Windows.Controls.Logic {

    public class EditorOperations: IEditorOperations {
        private CodeEditor _editor;
        private int _selectionAnchor;

        private double _desiredPosition;

        public EditorOperations(CodeEditor editor) {
            _editor = editor;
        }

        public void MoveToNextCharacter(bool select) {
            var pos = CaretPosition;
            var newPos = pos + CharSize(pos);
            MoveToNewPos(newPos, select);
            UpdateDesiredPosition();
        }

        public void MoveToPreviousCharacter(bool select) {
            var pos = CaretPosition;
            var newPos = pos - CharSize(pos - 1);
            MoveToNewPos(newPos, select);
            UpdateDesiredPosition();
        }

        public void MoveToNextWord(bool select) {
            var pos = CaretPosition;
            var newPos = WordForward(pos);
            MoveToNewPos(newPos, select);
            UpdateDesiredPosition();
        }

        public void MoveToPreviousWord(bool select) {
            var pos = CaretPosition;
            var newPos = WordBackward(pos);
            MoveToNewPos(newPos, select);
            UpdateDesiredPosition();
        }

        public void MoveLineUp(bool select) {
            ConditionalUpdateDesiredPosition();
            MoveToNewLine(CurrentLine - 1, select);
        }

        public void MoveLineDown(bool select) {
            ConditionalUpdateDesiredPosition();
            MoveToNewLine(CurrentLine + 1, select);
        }

        public void PageUp(bool select) {
            ScrollTo(FirstVisibleLine - PageSize);
            MoveToNewLine(CurrentLine - PageSize, select);
        }

        public void PageDown(bool select) {
            if (LastVisibleLine + 1 < TextBuffer.LineCount)
                ScrollTo(FirstVisibleLine + PageSize);
            MoveToNewLine(CurrentLine + PageSize, select);
        }

        public void MoveToEndOfLine(bool select) {
            var nextLine = TextBuffer.GetStartOfNextLine(CaretPosition);
            var endOfLine = nextLine - CharSize(nextLine - 1);
            MoveToNewPos(endOfLine, select);
            UpdateDesiredPosition();
        }

        public void MoveToStartOfLine(bool select) {
            MoveToNewPos(TextBuffer.GetStartOfLine(CurrentLine), select);
            UpdateDesiredPosition();
        }

        public void GotoLine(int lineNumber) {
            MoveToNewLine(lineNumber, false);
        }

        public void MoveToStartOfDocument(bool select) {
            MoveToNewPos(0, select);
            UpdateDesiredPosition();
        }

        public void MoveToEndOfDocument(bool select) {
            MoveToNewPos(TextBuffer.Length, select);
            UpdateDesiredPosition();
        }

        public void MoveCurrentLineToTop() {
            MoveToNewLine(FirstVisibleLine, false);
        }

        public void MoveCurrentLineToBottom() {
            MoveToNewLine(LastVisibleLine, false);
        }

        public void ScrollUpAndMoveCaretIfNecessary() {
            ScrollTo(FirstVisibleLine - 1);
            CrimpCaret();
        }

        public void ScrollDownAndMoveCaretIfNecessary() {
            ScrollTo(FirstVisibleLine + 1);
            CrimpCaret();
        }

        public void DeleteCharacterToLeft(UndoHistory undoHistory) {
            var pos = _editor.CaretPosition;
            var size = CharSize(pos - 1);
            var leftPos = pos - size;
            if (leftPos >= 0)
                _editor.TextBuffer.Delete(leftPos, size);
            UpdateDesiredPosition();
        }

        public void DeleteCharacterToRight(UndoHistory undoHistory) {
            var pos = CaretPosition;
            TextBuffer.Delete(pos, CharSize(pos));
            UpdateDesiredPosition();
        }

        public void DeleteWordToRight(UndoHistory undoHistory) {
            var pos = CaretPosition;
            var nextWord = WordForward(CaretPosition);
            if (pos < nextWord) {
                TextBuffer.Delete(pos, nextWord - pos);
                UpdateDesiredPosition();
            }
        }

        public void DeleteWordToLeft(UndoHistory undoHistory) {
            var pos = CaretPosition;
            var previousWord = WordBackward(pos);
            var endWord = WordForward(previousWord);
            if (pos > endWord)
                previousWord = endWord;
            if (previousWord < pos) {
                TextBuffer.Delete(previousWord, pos - previousWord);
                UpdateDesiredPosition();
            }
        }

        public void InsertNewline(UndoHistory undoHistory) {
            InsertText("\r\n", undoHistory);
            UpdateDesiredPosition();
        }

        public void InsertTab(UndoHistory undoHistory) {
            InsertText("\t", undoHistory);
            UpdateDesiredPosition();
        }

        public void InsertText(string text, UndoHistory undoHistory) {
            TextBuffer.Insert(_editor.CaretPosition, text);
            UpdateDesiredPosition();
        }

        public void InsertProvisionalText(string text, UndoHistory undoHistory) {
            throw new NotImplementedException();
        }

        public bool DeleteSelection(UndoHistory undoHistory) {
            var span = _editor.SelectionSpan;
            if (span != null) {
                TextBuffer.Delete(span.Start, span.Length);
                UpdateDesiredPosition();
                return true;
            }
            return false;
        }

        public void ReplaceSelection(string text, UndoHistory undoHistory) {
            var span = _editor.SelectionSpan;
            if (span != null) {
                TextBuffer.Replace(span.Start, span.Length, text);
                UpdateDesiredPosition();
            }
            else
                InsertText(text, undoHistory);
        }

        /*
        public void TransposeCharacter(UndoHistory undoHistory) {
            throw new NotImplementedException();
        }

        public void TransposeLine(UndoHistory undoHistory) {
            throw new NotImplementedException();
        }

        public void MakeLowercase(UndoHistory undoHistory) {
            throw new NotImplementedException();
        }

        public void MakeUppercase(UndoHistory undoHistory) {
            throw new NotImplementedException();
        }

        public void ReplaceText(Span replaceSpan, string text, UndoHistory undoHistory) {
            throw new NotImplementedException();
        }

        public int ReplaceAllMatches(string searchText, string replaceText, bool matchCase, bool matchWholeWord, bool useRegularExpressions, UndoHistory undoHistory) {
            throw new NotImplementedException();
        }
         */

        public void SelectCurrentWord() {
            var pos = CaretPosition;
            var wordEnd = WordForward(pos);
            var wordStart = WordBackward(pos);
            if (wordEnd > wordStart)
                _editor.SelectionSpan = SpanFor(wordStart, wordEnd);
        }

        public void SelectCurrentLine() {
            var line = CurrentLine;
            var bufferLine = TextBuffer.GetTextLine(line);
            MoveToNewPos(bufferLine.StartPoint.Position, false);
            MoveToNewPos(bufferLine.End, true);
            UpdateDesiredPosition();
        }

        /*
        public void SelectEnclosing() {
            throw new NotImplementedException();
        }

        public void SelectFirstChild() {
            throw new NotImplementedException();
        }

        public void SelectNextSibling(bool extendSelection) {
            throw new NotImplementedException();
        }

        public void SelectPreviousSibling(bool extendSelection) {
            throw new NotImplementedException();
        }

        public void SelectLine(int lineNumber, bool extendSelection) {
            throw new NotImplementedException();
        }
        */

        public void SelectAll() {
            MoveToNewPos(0, false);
            MoveToEndOfDocument(true);
            UpdateDesiredPosition();
        }

        public void ExtendSelection(int newEnd) {
            MoveToNewPos(newEnd, true);
            UpdateDesiredPosition();
        }

        public void ResetSelection() {
            _editor.SelectionSpan = null;
        }

        /*
        public void CopySelection() {
            throw new NotImplementedException();
        }

        public void CutSelection(UndoHistory undoHistory) {
            throw new NotImplementedException();
        }

        public void Paste(UndoHistory undoHistory) {
            throw new NotImplementedException();
        }

        public bool CanPaste {
            get { throw new NotImplementedException(); }
        }

        public bool OverwriteMode {
            get {
                throw new NotImplementedException();
            }
            set {
                throw new NotImplementedException();
            }
        }
        */

        private int CaretPosition {
            get {
                return _editor.CaretPosition;
            }
        }

        private int CurrentLine {
            get {
                return _editor.TextBuffer.GetLineContaining(_editor.CaretPosition);
            }
        }

        private int FirstVisibleLine {
            get {
                return _editor.FirstVisibleLine;
            }
        }

        private int LastVisibleLine {
            get {
                return _editor.LastVisibleLine;
            }
        }

        private ITextBuffer TextBuffer {
            get {
                return _editor.TextBuffer;
            }
        }

        private int PageSize {
            get {
                return _editor.PageSize;
            }
        }

        private void MoveToNewPos(int newPos, bool select) {
            var oldPos = _editor.CaretPosition;
            _editor.CaretPosition = newPos;
            newPos = _editor.CaretPosition; // Get the transformed new position back from the editor.

            if (select && oldPos != newPos) {
                var selection = _editor.SelectionSpan;
                if (selection == null || selection.Length == 0) {
                    _selectionAnchor = oldPos;
                    selection = SpanFor(oldPos, newPos);
                }
                else 
                    selection = SpanFor(_selectionAnchor, newPos);
                _editor.SelectionSpan = selection;
            }
            else
                _editor.SelectionSpan = null;
            _editor.EnsureCaretVisible();
        }

        private void MoveToNewLine(int newLine, bool select) {
            if (newLine < 0)
                newLine = 0;
            else if (newLine >= _editor.TextBuffer.LineCount)
                newLine = _editor.TextBuffer.LineCount - 1;
            MoveToNewPos(_editor.PositionOf(newLine, _desiredPosition), select);
        }

        private TextSpan ExtendSelectionToInclude(TextSpan selection, int newPos) {
            if (newPos < selection.Start)
                return new TextSpan(selection.TextBuffer, newPos, selection.End - newPos);
            else if (newPos > selection.End)
                return new TextSpan(selection.TextBuffer, selection.Start, newPos - selection.Start);
            else
                return selection;
        }

        private TextSpan SpanFor(int posA, int posB) {
            int start;
            int len;
            if (posA > posB) {
                start = posB;
                len = posA - posB;
            }
            else {
                start = posA;
                len = posB - posA;
            }
            if (len == 0)
                return null;
            return new TextSpan(_editor.TextBuffer, start, len);
        }

        private void UpdateDesiredPosition() {
            _desiredPosition = _editor.CaretDistance;
        }

        private void ConditionalUpdateDesiredPosition() {
            var caretDistance = _editor.CaretDistance;
            if (_desiredPosition < caretDistance)
                _desiredPosition = caretDistance;
        }

        private int CharSize(int pos) {
            var textBuffer = _editor.TextBuffer;
            var len = textBuffer.Length;
            if (pos < 0 || pos >= len)
                return 1;
            char ch = textBuffer.GetChar(pos);
            switch (ch) {
                case '\n': return pos > 0 ? textBuffer.GetChar(pos - 1) == '\r' ? 2 : 1 : 1;
                case '\r': return pos + 1 < len ? textBuffer.GetChar(pos + 1) == '\n' ? 2 : 1 : 1;
                default: return 1;
            }
        }

        private int WordForward(int position) {
            return WordForward(position, true);
        }

        private int WordForward(int position, bool forceNext) {
            var textBuffer = _editor.TextBuffer;
            if (position >= textBuffer.Length)
                return textBuffer.Length;
            var classifier = new WordClassifier();
            var line = textBuffer.GetLineContaining(position);
            var bufferLine = textBuffer.GetTextLine(line);
            var classifications = classifier.GetClassificationSpans(bufferLine.TextSpan).ToList();
            int i;
            ClassificationSpan classification = null;
            var lastPos = position + 1;
            for (i = 0; i < classifications.Count; i++) {
                var c = classifications[i];
                if (position >= c.TextSpan.Start && position < c.TextSpan.End) {
                    if (forceNext)
                        i++;
                    while (i < classifications.Count) {
                        c = classifications[i];
                        if (c.ClassificationType != "whitespace") {
                            classification = c;
                            break;
                        }
                        else
                            lastPos = c.TextSpan.End;
                        i++;
                    }
                    break;
                }
            }
            if (classification == null)
                return WordForward(lastPos, false);
            return classification.TextSpan.Start;
        }

        private int WordBackward(int position) {
            if (position <= 0)
                return 0;
            var textBuffer = _editor.TextBuffer;
            var classifier = new WordClassifier();
            var line = textBuffer.GetLineContaining(position);
            var bufferLine = textBuffer.GetTextLine(line);
            var classifications = classifier.GetClassificationSpans(bufferLine.TextSpan).ToList();
            int i;
            ClassificationSpan classification = null;
            for (i = classifications.Count - 1; i >= 0 ; i--) {
                var c = classifications[i];
                if (position >= c.TextSpan.Start && position < c.TextSpan.End) {
                    i--;
                    while (i >= 0) {
                        c = classifications[i];
                        if (c.ClassificationType != "whitespace") {
                            classification = c;
                            break;
                        }
                        i--;
                    }
                    break;
                }
            }
            if (classification == null)
                return WordBackward(position - 1);
            return classification.TextSpan.Start;
        }

        private void ScrollTo(int newLine) {
            var max = _editor.TextBuffer.LineCount;
            if (newLine < 0)
                newLine = 0;
            else if (newLine >= max)
                newLine = max - 1;
            _editor.DisplayLine(newLine);
        }

        private void CrimpCaret() {
            var caretLine = _editor.TextBuffer.GetLineContaining(_editor.CaretPosition);
            var firstVisibleLine = FirstVisibleLine;
            var lastUsableLine = firstVisibleLine + _editor.PageSize;
            while (caretLine < firstVisibleLine) {
                MoveLineDown(false);
                caretLine++;
            }
            while (caretLine > lastUsableLine) {
                MoveLineUp(false);
                caretLine--;
            }
        }

        private class WordClassifier : IClassifier {

            public IEnumerable<ClassificationSpan> GetClassificationSpans(TextSpan textSpan) {
                var textBuffer = textSpan.TextBuffer;
                var text = textBuffer.GetText(textSpan.Start, textSpan.Length);
                var wordScanner = new WordScanner(text);
                var pos = 0;
                var offset = textSpan.Start;
                while (wordScanner.NextWord()) {
                    if (pos != wordScanner.Start)
                        yield return new ClassificationSpan(new TextSpan(textBuffer, pos + offset, wordScanner.Start - pos), "whitespace");
                    yield return new ClassificationSpan(new TextSpan(textBuffer, wordScanner.Start + offset, wordScanner.End - wordScanner.Start), "word");
                    pos = wordScanner.End;
                }
                if (pos != wordScanner.Start)
                    yield return new ClassificationSpan(new TextSpan(textBuffer, pos + offset, wordScanner.Start - pos), "whitespace");
            }

            public event EventHandler<ClassificationChangedEventArgs> ClassificationChanged;

            private class WordScanner {
                private string _text;
                private int _start;
                private int _current;

                public WordScanner(string text) {
                    _text = text;
                }

                public int Start { get { return _start; } }
                public int End { get { return _current; } }

                public bool NextWord() {
                    var result = true;
                    var p = _current;
                    var max = _text.Length;
                    int start;
                    while (true) {
                        start = p;
                        char ch = GetChar(_text, p++, max);
                        switch (ch) {
                            case ' ': case '\t': case '\n': case '\r':
                                continue;
                            case '\0':
                                p--;
                                result = false;
                                break;
                            case '!': case '%': case '^': case '&': case '*': case '(': case ')':
                            case '{': case '}': case '-': case '+': case '=': case '[': case ']':
                            case ';': case ':': case ',': case '.': case '<': case '>': case '/':
                                while (true) {
                                    ch = GetChar(_text, p++, max);
                                    switch (ch) {
                                        case '!': case '%': case '^': case '&': case '*': case '(': case ')':
                                        case '{': case '}': case '-': case '+': case '=': case '[': case ']':
                                        case ';': case ':': case ',': case '.': case '<': case '>': case '/':
                                            continue;
                                        default:
                                            p--;
                                            break;
                                    }
                                    break;
                                }
                                break;
                            default:
                                while (true) {
                                    ch = GetChar(_text, p++, max);
                                    switch (ch) {
                                        case ' ': case '\t': case '\n': case '\r': case '\0':
                                        case '!': case '%': case '^': case '&': case '*': case '(': case ')':
                                        case '{': case '}': case '-': case '+': case '=': case '[': case ']':
                                        case ';': case ':': case ',': case '.': case '<': case '>': case '/':
                                            p--;
                                            break;
                                        default:
                                            continue;
                                    }
                                    break;
                                }
                                break;
                        }
                        break;
                    }
                    _start = start;
                    _current = p;
                    return result;
                }

                private static char GetChar(string text, int p, int max) {
                    return p < max ? text[p] : '\0';
                }
            }
        }
    }

}
